import hmac
import hashlib
import time
from starlette.types import ASGIApp, Receive, Scope, Send
from intelli_port.commons.context import corr_id_var


class CorrelationIdMiddleware:
    def __init__(self, app: ASGIApp):
        self.app = app

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope.get("type") == "http":
            headers = scope.get("headers", [])
            corr = None
            for k, v in headers:
                if k.decode().lower() == "x-correlation-id":
                    corr = v.decode()
                    break
            scope["corr_id"] = corr or str(int(time.time() * 1000))
            try:
                corr_id_var.set(scope["corr_id"])
            except Exception:
                pass
        await self.app(scope, receive, send)


class JavaSignatureMiddleware:
    def __init__(self, app: ASGIApp, secret: str, skew_seconds: int = 300):
        self.app = app
        self.secret = secret
        self.skew = skew_seconds

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope.get("type") != "http":
            await self.app(scope, receive, send)
            return
        headers = {k.decode().lower(): v.decode() for k, v in scope.get("headers", [])}
        ts = headers.get("x-java-timestamp")
        sig = headers.get("x-java-signature")
        if not self.secret:
            await self.app(scope, receive, send)
            return
        if not ts or not sig:
            await self._reject(send, 401)
            return
        try:
            tsv = int(ts)
        except Exception:
            await self._reject(send, 401)
            return
        now = int(time.time())
        if abs(now - tsv) > self.skew:
            await self._reject(send, 401)
            return
        body = b""
        more_body = True
        req = await receive()
        if req.get("type") == "http.request":
            body += req.get("body", b"")
            more_body = req.get("more_body", False)
        while more_body:
            req = await receive()
            body += req.get("body", b"")
            more_body = req.get("more_body", False)
        mac = hmac.new(self.secret.encode(), body + ts.encode(), hashlib.sha256).hexdigest()
        if mac != sig:
            await self._reject(send, 401)
            return
        async def new_receive():
            return {"type": "http.request", "body": body, "more_body": False}
        await self.app(scope, new_receive, send)

    async def _reject(self, send: Send, status_code: int):
        await send(
            {
                "type": "http.response.start",
                "status": status_code,
                "headers": [(b"content-type", b"application/json")],
            }
        )
        await send({"type": "http.response.body", "body": b"{}"})


class IdempotencyMiddleware:
    def __init__(self, app: ASGIApp, ttl_seconds: int = 300):
        self.app = app
        self.ttl = ttl_seconds
        self.cache = {}

    async def __call__(self, scope: Scope, receive: Receive, send: Send):
        if scope.get("type") != "http":
            await self.app(scope, receive, send)
            return
        method = scope.get("method", "GET").upper()
        if method in ("GET", "HEAD", "OPTIONS"):
            await self.app(scope, receive, send)
            return
        headers = {k.decode().lower(): v.decode() for k, v in scope.get("headers", [])}
        idem = headers.get("x-idempotency-key")
        if not idem:
            await self.app(scope, receive, send)
            return
        # Read the body
        body = b""
        more_body = True
        req = await receive()
        if req.get("type") == "http.request":
            body += req.get("body", b"")
            more_body = req.get("more_body", False)
        while more_body:
            req = await receive()
            body += req.get("body", b"")
            more_body = req.get("more_body", False)
        # Check redis, fallback to in-memory cache if unavailable
        key = f"idem:{idem}"
        try:
            r = None
            try:
                r = getattr(self.app.state, "redis", None)
            except Exception:
                r = None
            if r is None:
                from intelli_port.data_clients.redis import get_redis
                r = get_redis()
            if r.get(key):
                await send(
                    {
                        "type": "http.response.start",
                        "status": 409,
                        "headers": [(b"content-type", b"application/json")],
                    }
                )
                await send({"type": "http.response.body", "body": b"{\"error\":{\"code\":\"IDEMPOTENT\",\"message\":\"Duplicate request\"}}"})
                try:
                    from intelli_port.commons.observability import middleware_idempotency_failures_total
                    middleware_idempotency_failures_total.labels(reason="duplicate").inc()
                except Exception:
                    pass
                return
            r.setex(key, self.ttl, b"1")
        except Exception:
            try:
                from intelli_port.commons.observability import middleware_idempotency_failures_total
                middleware_idempotency_failures_total.labels(reason="redis_unavailable").inc()
            except Exception:
                pass
            now = time.time()
            exp = self.cache.get(key, 0.0)
            if exp and now < exp:
                await send(
                    {
                        "type": "http.response.start",
                        "status": 409,
                        "headers": [(b"content-type", b"application/json")],
                    }
                )
                await send({"type": "http.response.body", "body": b"{\"error\":{\"code\":\"IDEMPOTENT\",\"message\":\"Duplicate request\"}}"})
                return
            self.cache[key] = now + self.ttl
        async def new_receive():
            return {"type": "http.request", "body": body, "more_body": False}
        await self.app(scope, new_receive, send)